#region Copyright Notice
/*
Copyright (C) 2011 by Ashley Ross

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#endregion Copyright Notice

using System;
using System.IO;

namespace Manta.IO {
    /// <summary>
    /// Provides methods for managing junctions.
    /// </summary>
    public static class Junction {
        /// <summary>
        /// Creates a junction at the specified alias pointing to the specified target.
        /// </summary>
        /// <param name="alias">The directory path to use as the alias.</param>
        /// <param name="target">The path to the target directory.</param>
        /// <returns>A JunctionInfo instance that provides access to the properties of the created junction.</returns>
        /// <remarks>If the alias directory does not exist, it will be created; if it does exist, it must be empty.</remarks>
        public static JunctionInfo Create(string alias, string target) {
            // Defer invalid character parsing to the DirectoryInfo constructor
            return Create(new DirectoryInfo(alias), new DirectoryInfo(target), null);
        }

        /// <summary>
        /// Creates a junction at the specified alias pointing to the specified target with the specified print name.
        /// </summary>
        /// <param name="alias">The directory path to use as the alias.</param>
        /// <param name="target">The path to the target directory.</param>
        /// <param name="printName">The display name for the junction.</param>
        /// <returns>A JunctionInfo instance that provides access to the properties of the created junction.</returns>
        /// <remarks>If the alias directory does not exist, it will be created; if it does exist, it must be empty.</remarks>
        public static JunctionInfo Create(string alias, string target, string printName) {
            // Defer invalid character parsing to the DirectoryInfo constructor
            return Create(new DirectoryInfo(alias), new DirectoryInfo(target), printName);
        }

        /// <summary>
        /// Creates a junction at the specified alias pointing to the specified target.
        /// </summary>
        /// <param name="alias">The directory to use as the alias.</param>
        /// <param name="target">The target directory.</param>
        /// <returns>A JunctionInfo instance that provides access to the properties of the created junction.</returns>
        /// <remarks>If the alias directory does not exist, it will be created; if it does exist, it must be empty.</remarks>
        public static JunctionInfo Create(DirectoryInfo alias, DirectoryInfo target) {
            return Create(alias, target, null);
        }

        /// <summary>
        /// Creates a junction at the specified alias pointing to the specified target with the specified print name.
        /// </summary>
        /// <param name="alias">The directory to use as the alias.</param>
        /// <param name="target">The target directory.</param>
        /// <param name="printName">The display name for the junction.</param>
        /// <returns>A JunctionInfo instance that provides access to the properties of the created junction.</returns>
        /// <remarks>If the alias directory does not exist, it will be created; if it does exist, it must be empty.</remarks>
        public static JunctionInfo Create(DirectoryInfo alias, DirectoryInfo target, string printName)
        {
            return Create(alias, target, printName, null, null, null);
        }

        internal static JunctionInfo Create(DirectoryInfo alias, DirectoryInfo target, string printName, DateTime? creationTime, DateTime? lastWriteTime, DateTime? lastAccessTime)
        {
            ReparseDataBuffer reparseData = new ReparseDataBuffer(ReparseTag.MountPoint);
            bool createdAlias = false, createdJunction = false;

            if (alias == null)
                throw new ArgumentNullException("alias");

            alias.Refresh();

            if (alias.FullName == alias.Root.FullName)
                throw new ArgumentException("The alias parameter must be a non-root directory.", "alias");

            // Ensure that junctions are supported on this volume.
            //
            // Theoretically, one could also check for the FILE_SUPPORTS_REPARSE_POINTS bit,
            // but that assumes that junctions aren't actually implemented by the NTFS driver.
            if (new DriveInfo(alias.Root.FullName).DriveFormat != "NTFS")
                throw new NotSupportedException("Junctions are only supported on NTFS volumes.");

            if (target == null)
                throw new ArgumentNullException("target");

            target.Refresh();

            if (!target.Exists)
                throw new DirectoryNotFoundException(string.Format(System.Globalization.NumberFormatInfo.CurrentInfo, "The target directory, \"{0}\", does not exist.", target.FullName));

            // Create the alias directory if it doesn't exist
            if (!alias.Exists)
            {
                alias.Create();
                // Clear the DirectoryInfo instance's cached state
                alias.Refresh();
                createdAlias = true;

                // The alias directory exists, so make sure that it's empty (Directories can
                // only have reparse points set if they are empty)
            }
            else if (alias.GetFileSystemInfos().Length > 0 & !ReparsePoint.Exists(alias))
            {
                throw new InvalidOperationException(string.Format(System.Globalization.NumberFormatInfo.CurrentInfo, "The alias directory, \"{0}\", is not empty.", alias.FullName));
            }

            if (creationTime != null)
                alias.CreationTime = creationTime.Value;
            if (lastWriteTime != null)
                alias.LastWriteTime = lastWriteTime.Value;
            if (lastAccessTime != null)
                alias.LastAccessTime = lastAccessTime.Value;

            // Set the names on the reparse structure; it has its own input sanitation.
            reparseData.SubstituteName = @"\??\" + target.FullName;
            reparseData.PrintName = printName ?? target.FullName;

            try
            {
                ReparsePoint.Set(alias, reparseData);
                createdJunction = true;
                return new JunctionInfo(alias, target, reparseData.PrintName, true);
            }
            finally
            {
                // Remove the created alias directory if setting the reparse point fails
                if (createdAlias && !createdJunction)
                {
                    alias.Delete(false);
                }
            }
        }

        /// <summary>
        /// Deletes the specified junction directory.
        /// </summary>
        /// <param name="alias">The path to the directory to delete.</param> 
        /// <remarks>This method deletes the specified directory even if it does not contain a junction; however, it will not delete non-empty directories.</remarks>
        public static void Delete(string alias) {
            Delete(new DirectoryInfo(alias));
        }

        /// <summary>
        /// Deletes the specified junction directory.
        /// </summary>
        /// <param name="alias">The directory to delete.</param> 
        /// <remarks>This method deletes the specified directory even if it does not contain a junction; however, it will not delete non-empty directories.</remarks>
        public static void Delete(DirectoryInfo alias) {
            if (alias == null)
                throw new ArgumentNullException("alias");

            alias.Delete(false);
        }

        /// <summary>
        /// Determines whether the specified directory exists and whether it contains a junction.
        /// </summary>
        /// <param name="alias">The path to the directory to check.</param>
        /// <returns>true if the directory exists and contains a junction; otherwise, false.</returns>
        public static bool Exists(string alias) {
            if (string.IsNullOrEmpty(alias))
                return false;

            try {
                return Exists(new DirectoryInfo(alias));
            } catch (Exception) { /* Quash any exceptions */ }

            return false;
        }

        /// <summary>
        /// Determines whether the specified directory exists and whether it contains a junction.
        /// </summary>
        /// <param name="alias">The directory to check.</param>
        /// <returns>true if the directory exists and contains a junction; otherwise, false.</returns>
        public static bool Exists(DirectoryInfo alias) {
            try {
                return ReparsePoint.Exists(alias) && (ReparsePoint.Get<ReparseDataBuffer>(alias).Tag == ReparseTag.MountPoint);
            } catch (Exception) { /* Quash any exceptions */ }

            return false;
        }

        /// <summary>
        /// Reads the specified junction directory into a new JunctionInfo instance.
        /// </summary>
        /// <param name="alias">The path to the alias directory.</param>
        /// <returns>A JunctionInfo instance that provides access to the properties of the junction.</returns>
        public static JunctionInfo Get(string alias) {
            return new JunctionInfo(alias);
        }

        /// <summary>
        /// Reads the specified junction directory into a new JunctionInfo instance.
        /// </summary>
        /// <param name="alias">The alias directory.</param>
        /// <returns>A JunctionInfo instance that provides access to the properties of the junction.</returns>
        public static JunctionInfo Get(DirectoryInfo alias) {
            return new JunctionInfo(alias);
        }
    }
}